#include "Wt.h"

/* The table object provides a multimap. */

/* Get the internal hash table */

Tcl_HashTable *WtGetTableMap(Tcl_Obj *tbl)
{
  WtTable *tblRep = WtToTable(tbl);
  return &(tblRep->items);
}

/* Initialize the underlying hash table */

void WtInitTableItems(Tcl_Obj *tbl)
{
  WtTable *tblRep = WtToTable(tbl);

  if (tbl->typePtr == &WtTableType) {
    Tcl_InitObjHashTable(&tblRep->items);
  } else if (tbl->typePtr == &WtStringTableType) {
    Tcl_InitCustomHashTable(&tblRep->items,
      TCL_CUSTOM_PTR_KEYS, &WtTableCaseKeyType);
  }
}

/* Delete contents of a table */

void WtDeleteTableItems(Tcl_Obj *tbl)
{
  Tcl_HashTable *hashTable = WtGetTableMap(tbl);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {
    Tcl_DecrRefCount((Tcl_Obj *)Tcl_GetHashValue(ent));
    ent = Tcl_NextHashEntry(&search);
  }

  Tcl_DeleteHashTable(hashTable);
}

/* Get the size of a table */

int WtTableSize(Tcl_Obj *tbl)
{
  return WtGetTableMap(tbl)->numEntries;
}

/* Get the first matching item */

Tcl_Obj *WtTableGet(Tcl_Obj *tbl, Tcl_Obj *key)
{
  Tcl_Obj *list;
  Tcl_HashEntry *ent = Tcl_FindHashEntry(WtGetTableMap(tbl), (char *)key);

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, WtGetContext(),
          "WtTableGet (tbl = 0x%x, key = 0x%x)", tbl, key);

  if (ent) {
    list = (Tcl_Obj *)(Tcl_GetHashValue(ent));
    if (WtGetListSize(list)) {
      return WtGetFirstListElement(list);
    }
  }
  return NULL;  
}

/* Get the list of matching items */

Tcl_Obj *WtTableGetAll(Tcl_Obj *tbl, Tcl_Obj *key)
{
  Tcl_HashEntry *ent;
  ent = Tcl_FindHashEntry(WtGetTableMap(tbl), (char *)key);
  if (ent) {
    return (Tcl_Obj *)(Tcl_GetHashValue(ent));
  }
  return NULL;
}

/* Get the first matching item */

Tcl_Obj *WtTableGetDefault(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *fallback)
{
  Tcl_Obj *val = WtTableGet(tbl, key);
  return val ? val : fallback;
}

/* Set a table entry */

void WtTableSet(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *val)
{
  int created;
  Tcl_HashEntry *ent;
  Tcl_Obj *list;

  ent = Tcl_CreateHashEntry(WtGetTableMap(tbl), (char *)key, &created);

  if (ent) {
    if (!created) {
      Tcl_DecrRefCount((Tcl_Obj *)Tcl_GetHashValue(ent));
    }
    list = Tcl_NewListObj(1, &val);
    Tcl_SetHashValue(ent, list);
    Tcl_IncrRefCount(list);
  }
}

/* Set an entry to multiple items */

void WtTableSetAll(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *list)
{
  int created;
  Tcl_HashEntry *ent;
  ent = Tcl_CreateHashEntry(WtGetTableMap(tbl), (char *)key, &created);

  if (ent) {
    if (!created) {
      Tcl_DecrRefCount((Tcl_Obj *)Tcl_GetHashValue(ent));
    }
    list = Tcl_DuplicateObj(list);
    Tcl_SetHashValue(ent, list);
    Tcl_IncrRefCount(list);
  }
}

/* Set a table entry if it doesn't exist */

void WtTableSetDefault(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *val)
{
  Tcl_HashEntry *ent;

  ent = Tcl_FindHashEntry(WtGetTableMap(tbl), (char *)key);

  if (!ent) {
    WtTableSet(tbl, key, val);
  }
}

/* Add a table item */

void WtTableAdd(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *val)
{
  int created;
  Tcl_HashEntry *ent;
  Tcl_Obj *list;

  ent = Tcl_CreateHashEntry(WtGetTableMap(tbl), (char *)key, &created);

  if (ent) {
    if (created) {
      list = Tcl_NewListObj(1, &val);
      Tcl_IncrRefCount(list);
      Tcl_SetHashValue(ent, list);
    } else {
      WtAppendToList((Tcl_Obj *)Tcl_GetHashValue(ent), val);
    }
  }
}

/* Add a lsit of values */

void WtTableAddList(Tcl_Obj *tbl, Tcl_Obj *key, Tcl_Obj *list)
{
  int created;
  Tcl_HashEntry *ent;

  ent = Tcl_CreateHashEntry(WtGetTableMap(tbl), (char *)key, &created);

  if (ent) {
    if (created) {
      /* XXX: Duplicate or incr ref? */
      list = Tcl_DuplicateObj(list);
      Tcl_SetHashValue(ent, list);
    } else {
      WtAppendListToList((Tcl_Obj *)Tcl_GetHashValue(ent), list);
    }
  }
}

/* Get all table keys */

Tcl_Obj *WtTableKeys(Tcl_Obj *tbl)
{
  Tcl_Obj *list = Tcl_NewListObj(0, NULL);
  Tcl_HashTable *hashTable = WtGetTableMap(tbl);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {
    WtAppendToList(list, (Tcl_Obj *)Tcl_GetHashKey(hashTable, ent));
    ent = Tcl_NextHashEntry(&search);
  }

  return list;
}

/* Get the first item for each entry */

Tcl_Obj *WtTableValues(Tcl_Obj *tbl)
{
  Tcl_Obj *list = Tcl_NewListObj(0, NULL), *obj;
  Tcl_HashTable *hashTable = WtGetTableMap(tbl);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {
    WtAppendToList(list, (Tcl_Obj *)Tcl_GetHashKey(hashTable, ent));

    obj = WtGetFirstListElement((Tcl_Obj *)Tcl_GetHashValue(ent));

    if (obj) {
      WtAppendToList(list, obj);
    }
    ent = Tcl_NextHashEntry(&search);
  }

  return list;
}

/* Get all table items */

Tcl_Obj *WtTableAllValues(Tcl_Obj *tbl)
{
  Tcl_Obj *list = Tcl_NewListObj(0, NULL);
  Tcl_HashTable *hashTable = WtGetTableMap(tbl);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {
    if (Tcl_ListObjAppendElement(NULL, list,
        Tcl_GetHashValue(ent)) != TCL_OK) {
      ap_assert(0);
    }
    ent = Tcl_NextHashEntry(&search);
  }

  return list;
}

/* Remove a table item */

void WtTableRemove(Tcl_Obj *tbl, Tcl_Obj *key)
{
  Tcl_HashEntry *ent = Tcl_FindHashEntry(WtGetTableMap(tbl), (char *)key);
  Tcl_Obj *val;
  if (ent) {
    val = (Tcl_Obj *)Tcl_GetHashValue(ent);
    if (val) {
      Tcl_DecrRefCount(val);
    }
    Tcl_DeleteHashEntry(ent);
  }
}

/* Remove a list of table items by key */

void WtTableRemoveList(Tcl_Obj *tbl, Tcl_Obj *list)
{
  int i, len;
  Tcl_Obj *key;

  if (Tcl_ListObjLength(NULL, list, &len) != TCL_OK) {
    ap_assert(NULL);
  }

  for (i = 0; i < len; i++) {
    key = WtGetListItem(list, i);
    if (key) {
      WtTableRemove(tbl, key);
    }
  }
}

/* Merge two tables */

void WtUpdateTable(Tcl_Obj *dst, Tcl_Obj *src)
{
  Tcl_HashTable *hashTable = WtGetTableMap(src);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;
  Tcl_Obj *key, *val;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {
    key = (Tcl_Obj *)Tcl_GetHashKey(hashTable, ent);
    val = (Tcl_Obj *)Tcl_GetHashValue(ent);
    WtTableSetAll(dst, key, val);
    ent = Tcl_NextHashEntry(&search);
  }
}

/* Clear the contents of a table */

void WtClearTable(Tcl_Obj *tbl)
{
  WtDeleteTableItems(tbl);
  WtInitTableItems(tbl);
}

/* Create a table from a string */

int WtStringToTable(Tcl_Interp *interp, Tcl_Obj *str, Tcl_Obj *tbl,
                    Tcl_Obj **newTbl, Tcl_ObjType *type)
{
  int ret = TCL_ERROR;
  WtTable *tblRep = NULL;
  Tcl_Obj *dest = tbl;

  *newTbl = NULL;

  /* Init string */

  WtToString(str);

  /* An empty string can be considered an empty table */

  if (str->length == 0) {
    tblRep = (WtTable *)ckalloc(sizeof(WtTable));
  }

  if (tblRep) {
    if (tbl) {
      if (tbl->typePtr && tbl->typePtr->freeIntRepProc) {
        tbl->typePtr->freeIntRepProc(tbl);
      }
    } else if (newTbl) {
      dest = WtNewTableObjWithType(type);
      *newTbl = dest;
    }

    if (dest) {
      dest->internalRep.twoPtrValue.ptr1 = tblRep;
      dest->typePtr = type;
      WtInitTableItems(dest);
      ret = TCL_OK;
    }
  }

  if (ret != TCL_OK) {
    Tcl_AppendStringsToObj(Tcl_GetObjResult(interp),
      "Object could not be converted to a table.", NULL);
  }

  return ret;
}

/* Define a custom hash table for case-insensitive
   string lookups */

/* Caseless string map hashKeyProc */

static unsigned int HashCaseKey(Tcl_HashTable *hashTable, void *keyPtr)
{
  Tcl_Obj *key = (Tcl_Obj *)keyPtr;
  const char *string;
  int length, i;
  unsigned int result = 0;

  string = WtToString(key);
  length = key->length;

  for (i = 0; i < length; i++) {
    result += (result << 3) + tolower(string[i]);
  }

  return result;  
}

/* Caseless string map compareKeysProc */

static int CompareCaseKeys(void *keyPtr, Tcl_HashEntry *entry)
{
  Tcl_Obj *obj1 = (Tcl_Obj *)keyPtr;
  Tcl_Obj *obj2 = (Tcl_Obj *)(entry->key.oneWordValue);
  const char *p1, *p2;
  int l1, l2;

  if (obj1 == obj2) {
      return 1;
  }

  p1 = WtToString(obj1);
  l1 = obj1->length;

  p2 = WtToString(obj2);
  l2 = obj2->length;

  if (l1 == l2) {
    while (l1--) {
      if (tolower(*p1) != tolower(*p2)) {
        return 0;
      }
      p1++;
      p2++;
    }
    return 1;
  }

  return 0;
}

/* Caseless string map allocEntryProc */

static Tcl_HashEntry *AllocCaseEntry(Tcl_HashTable *hashTable, void *keyPtr)
{
  Tcl_Obj *key = (Tcl_Obj *)keyPtr;
  Tcl_HashEntry *ent;
  ent = (Tcl_HashEntry *)ckalloc((unsigned)(sizeof(Tcl_HashEntry)));
  ent->key.oneWordValue = (char *)key;
  Tcl_IncrRefCount(key);
  return ent;
}

/* Caseless string map freeEntryProc */

static void FreeCaseEntry(Tcl_HashEntry *ent)
{
  Tcl_Obj *key = (Tcl_Obj *)ent->key.oneWordValue;
  Tcl_DecrRefCount(key);
  ckfree((char *)ent);
}

/* Caseless string hash key type */

Tcl_HashKeyType WtTableCaseKeyType = {
  TCL_HASH_KEY_TYPE_VERSION,          /* version */
  0,                                  /* flags */
  HashCaseKey,                        /* hashKeyProc */
  CompareCaseKeys,                    /* compareKeysProc */
  AllocCaseEntry,                     /* allocEntryProc */
  FreeCaseEntry                       /* freeEntryProc */
};

/* Map a command argument to a table. Retrieves an existing table
   if appropriate, or creates a new one. */

Tcl_Obj *WtGetTableRef(Tcl_Obj *obj, Tcl_ObjType *type, int *created, Tcl_Interp *interp)
{
  Tcl_Obj *found;

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, WtGetContext(),
          "WtGetTableRef: obj = 0x%x, type = 0x%x", obj, type);

  *created = 0;

  if (!type) {

    /* Is it a table? */

    if (obj->typePtr == &WtTableType || obj->typePtr == &WtStringTableType) {
      found = obj;
    }

    /* Lookup an obj table */

    if (!found) {
      found = WtGetPtr(obj, &WtStringTableType);
    }

    /* Lookup a string table */

    if (!found) {
      found = WtGetPtr(obj, &WtStringTableType);
    }

    /* Can it be converted to a table? */

    if (!found && WtStringToTable(interp, obj, NULL, &found, type) == TCL_OK) {
      *created = 1;
    }

  } else {

    found = WtGetPtr(obj, type);

    if (!found && WtStringToTable(interp, obj, NULL, &found, type) == TCL_OK) {
      *created = 1;
    }

  }

  return found;
}

int WtTableStrExists(Tcl_Obj *tbl, const char *key)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtTableGet(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);

  return valObj != NULL;
}

void WtTableSetStrToStr(Tcl_Obj *tbl, const char *key, const char *val)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtNewString(val);
  Tcl_IncrRefCount(valObj);

  WtTableSet(tbl, keyObj, valObj);

  Tcl_DecrRefCount(keyObj);
  Tcl_DecrRefCount(valObj);
}

void WtTableAddStrToStr(Tcl_Obj *tbl, const char *key, const char *val)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtNewString(val);
  Tcl_IncrRefCount(valObj);

  WtTableAdd(tbl, keyObj, valObj);

  Tcl_DecrRefCount(keyObj);
  Tcl_DecrRefCount(valObj);
}

void WtTableSetStrToInt(Tcl_Obj *tbl, const char *key, int val)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = Tcl_NewIntObj(val);
  Tcl_IncrRefCount(valObj);

  WtTableSet(tbl, keyObj, valObj);

  Tcl_DecrRefCount(keyObj);
  Tcl_DecrRefCount(valObj);
}

void WtTableSetStrToBool(Tcl_Obj *tbl, const char *key, int val)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = Tcl_NewBooleanObj(val);
  Tcl_IncrRefCount(valObj);

  WtTableSet(tbl, keyObj, valObj);

  Tcl_DecrRefCount(keyObj);
  Tcl_DecrRefCount(valObj);
}

void WtTableSetStrToObj(Tcl_Obj *tbl, const char *key, Tcl_Obj *val)
{
  Tcl_Obj *keyObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  WtTableSet(tbl, keyObj, val);

  Tcl_DecrRefCount(keyObj);
}

char *WtTableGetStrFromStr(Tcl_Obj *tbl, const char *key, char *fallback)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtTableGet(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);

  return valObj ? WtToString(valObj) : fallback;
}

int WtTableGetIntFromStr(Tcl_Obj *tbl, const char *key, int fallback)
{
  int ret = fallback, intVal;
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtTableGet(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);

  if (valObj) {
    if (Tcl_GetIntFromObj(NULL, valObj, &intVal) == TCL_OK) {
      ret = intVal;
    }
  }

  return ret;
}

int WtTableGetBoolFromStr(Tcl_Obj *tbl, const char *key, int fallback)
{
  int ret = fallback, intVal;
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtTableGet(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);

  if (valObj) {
    if (Tcl_GetBooleanFromObj(NULL, valObj, &intVal) == TCL_OK) {
      ret = intVal;
    }
  }

  return ret;
}

Tcl_Obj *WtTableGetObjFromStr(Tcl_Obj *tbl, const char *key, Tcl_Obj *fallback)
{
  Tcl_Obj *keyObj, *valObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  valObj = WtTableGet(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);

  return valObj ? valObj : fallback;
}

void WtTableRemoveStr(Tcl_Obj *tbl, const char *key)
{
  Tcl_Obj *keyObj;

  keyObj = WtNewString(key);
  Tcl_IncrRefCount(keyObj);

  WtTableRemove(tbl, keyObj);

  Tcl_DecrRefCount(keyObj);
}

/* Update a WtTable from an apache table */

void WtUpdateTableFromApTable(Tcl_Obj *tbl, table *apTable)
{
  int i;
  array_header *arr;
  table_entry *elts;
  char *key, *val;
  Tcl_Obj *keyObj, *valObj;

  arr = ap_table_elts(apTable);
  elts = (table_entry *)arr->elts;

  for (i = 0; i < arr->nelts; i++) {
    key = elts[i].key ? elts[i].key : "";
    val = elts[i].val ? elts[i].val : "";

    keyObj = Tcl_NewStringObj(key, -1);
    Tcl_IncrRefCount(keyObj);

    valObj = Tcl_NewStringObj(val, -1);
    Tcl_IncrRefCount(valObj);

    WtTableAdd(tbl, keyObj, valObj);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, WtGetContext(),
            "WtUpdateTableFromApTable: Adding: key = %s, val = %s",
            WtToString(keyObj), WtToString(valObj));

    Tcl_DecrRefCount(keyObj);
    Tcl_DecrRefCount(valObj);
  }
}

/* Update an apache table from a WtTable */

void WtUpdateApTableFromTable(table *apTable, Tcl_Obj *tbl)
{
  Tcl_HashTable *hashTable = WtGetTableMap(tbl);
  Tcl_HashEntry *ent;
  Tcl_HashSearch search;
  Tcl_Obj *list, *obj;
  char *key, *val;
  int i, len;

  ent = Tcl_FirstHashEntry(hashTable, &search);

  while (ent) {

    key = WtToString((Tcl_Obj *)Tcl_GetHashKey(hashTable, ent));

    ap_table_unset(apTable, key);

    list = (Tcl_Obj *)Tcl_GetHashValue(ent);

    if (list) {

      len = WtGetListSize(list);

      for (i = 0; i < len; i++) {
        obj = WtGetListItem(list, i);
        val = Tcl_GetStringFromObj(obj, NULL);
        ap_table_add(apTable, key, val);
      }

    }

    ent = Tcl_NextHashEntry(&search);
  }
}

/* Create a new table object of a certain type */

Tcl_Obj *WtNewTableObjWithType(Tcl_ObjType *type)
{
  WtTable *tbl;
  Tcl_Obj *obj;

  tbl = (WtTable *)ckalloc(sizeof(WtTable));
  obj = Tcl_NewObj();
  obj->internalRep.twoPtrValue.ptr1 = tbl;
  obj->typePtr = type ? type : &WtTableType;
  WtInitTableItems(obj);
  Tcl_InvalidateStringRep(obj);

  return obj;
}

/* Get the underlying table from a Tcl object */

WtTable *WtToTable(Tcl_Obj *obj)
{
  return (WtTable *)(obj->internalRep.twoPtrValue.ptr1);
}

/* Table object procs */

static void WtFreeTable(Tcl_Obj *tbl)
{
  WtDeletePtr(tbl);
  WtDeleteTableItems(tbl);
  ckfree((char *)WtToTable(tbl));
}

static void WtDupTable(Tcl_Obj *src, Tcl_Obj *dst)
{
  WtTable *srcRep = WtToTable(src), *dstRep;
  Tcl_HashTable *srcTable = WtGetTableMap(src), *dstTable;
  Tcl_HashEntry *srcEnt, *dstEnt;
  Tcl_HashSearch search;
  Tcl_Obj *list;
  int created;

  dstRep = (WtTable *)ckalloc(sizeof(WtTable));
  dst->internalRep.twoPtrValue.ptr1 = dstRep;
  dst->typePtr = src->typePtr;
  WtInitTableItems(dst);
  dstTable = WtGetTableMap(dst);

  srcEnt = Tcl_FirstHashEntry(srcTable, &search);

  while (srcEnt) {
    dstEnt = Tcl_CreateHashEntry(dstTable, Tcl_GetHashKey(srcTable, srcEnt), &created);
    list = (Tcl_Obj *)Tcl_GetHashValue(srcEnt);
    if (list) {
      list = Tcl_DuplicateObj(list);
    }
    Tcl_SetHashValue(dstEnt, list);
    srcEnt = Tcl_NextHashEntry(&search);
  }
}

static void WtUpdateTableString(Tcl_Obj *tbl)
{
  WtUpdatePtrString(tbl);
}

static int WtSetTableFromAny(Tcl_Interp *interp, Tcl_Obj *obj, Tcl_ObjType *type)
{
  int ret = WtStringToTable(interp, obj, obj, NULL, type);
  return ret;
}

/* Create a new table object */

Tcl_Obj *WtNewTableObj(char *name)
{
  Tcl_Obj *obj = WtNewTableObjWithType(&WtTableType);
  if (name) {
    WtSetPtrId(obj, name, -1);
  }
  return obj;
}

/* Table object type */

static int WtSetObjTableFromAny(Tcl_Interp *interp, Tcl_Obj *obj)
{
  return WtSetTableFromAny(interp, obj, &WtTableType);
}

struct Tcl_ObjType WtTableType =
{
  "table",
  WtFreeTable,
  WtDupTable,
  WtUpdateTableString,
  WtSetObjTableFromAny
};

/* Create a new string table object */

Tcl_Obj *WtNewStringTableObj(char *name)
{
  Tcl_Obj *obj = WtNewTableObjWithType(&WtStringTableType);
  if (name) {
    WtSetPtrId(obj, name, -1);
  }
  return obj;
}

/* String table object type */

static int WtSetStringTableFromAny(Tcl_Interp *interp, Tcl_Obj *obj)
{
  return WtSetTableFromAny(interp, obj, &WtTableType);
}

struct Tcl_ObjType WtStringTableType =
{
  "strtable",
  WtFreeTable,
  WtDupTable,
  WtUpdateTableString,
  WtSetStringTableFromAny
};
